#pragma once
#include <brpc/channel.h>
#include <vector>
#include <string>
#include <unordered_map>
#include <mutex>
#include "logger.hpp"

/*
*    192.168.32.1:8080 -- /server/friend
*    192.168.32.2:8080 -- /server/friend 
*    192.168.32.3:8080 -- /server/friend
*  例如, 三个服务器都部署了相同服务，那么ServiceChannel就可以将这些相同的服务对应地址管理起来，以轮转的形式，进行访问获取服务
*  但是我们又不能对每一个服务都实例化一个ServiceChannel，所以我们又封装一层ServiceManager 作为服务信道的管理类
*/

namespace im
{
// 封装单个服务的信道管理类
class ServiceChannel
{
public:
    using ptr = std::shared_ptr<ServiceChannel>;
    using ChannelPtr = std::shared_ptr<brpc::Channel>;
private:
    std::string _service_name;         // 服务名
    std::vector<ChannelPtr> _channels; // 服务对应的多个channel
    std::unordered_map<std::string, ChannelPtr> _hosts; // 主机地址与ChannelPtr映射关系
    int32_t _index;                    // 用于轮转的下标
    std::mutex _mutex;                 // 互斥锁，保证操作容器是线程安全的
public:
    ServiceChannel(const std::string& name)
        : _service_name(name)
        , _index(0)
    {}
    // 服务上线了一个节点，则调用append新增channel
    void append(const std::string& host)
    {
        auto channel = std::make_shared<brpc::Channel>();
        brpc::ChannelOptions options;
        options.connect_timeout_ms = -1;  // connect连接超时时间，-1表示一直等待
        options.timeout_ms = -1;          // rpc请求超时时间, -1表示一直等待
        options.max_retry = 3;            // 请求重试次数
        options.protocol = "baidu_std";   // 序列化协议，默认使用baidu_std
        int ret = channel->Init(host.c_str(), &options);
        if (ret == -1) {
            LOG_ERROR("初始化{}-{}信道失败!", _service_name, host);
            return;
        }

        std::unique_lock<std::mutex> lock(_mutex);
        _channels.push_back(channel);
        _hosts.insert(std::make_pair(host, channel));
    }
    // 服务下线了一个节点，则调用remove释放信道
    void remove(const std::string& host)
    {
        std::unique_lock<std::mutex> lock(_mutex);

        auto it = _hosts.find(host);
        if (it == _hosts.end()) {
            LOG_WARN("{}-{}节点删除时，没有找到信道信息!", _service_name, host);
            return;
        }
        for (auto sit = _channels.begin(); sit != _channels.end(); sit++)
        {
            if (*sit == it->second) {
                _channels.erase(sit);
                break;
            }
        } 
        _hosts.erase(host);
    }
    ChannelPtr choose()
    {
        std::unique_lock<std::mutex> lock(_mutex);
        if (_channels.size() == 0) {
            LOG_ERROR("当前没有能够提供{}服务的节点!", _service_name);
            return ChannelPtr();
        }
        int32_t index = index++ % _channels.size();
        return _channels[index];
    }
};

//总体的服务信道管理类
class ServiceManager
{
public:
    using ptr = std::shared_ptr<ServiceManager>;
private:
    std::mutex _mutex;
    std::unordered_set<std::string> _follow_services;               // 关心的服务都有哪些
    std::unordered_map<std::string, ServiceChannel::ptr> _services; // 各服务对应的ServiceChannel
    std::string getServiceName(const std::string& service_instance)
    {
        // /service/echo/instance 这里其实提供的就是/service/echo服务，后面的instance是用来区分echo的
        // 因为如果相同服务都使用相同的名字，那么向etcd进行注册键值对时，都会发生上一次数据的覆盖，我们需要保存所有键值对信息，
        // 而Discovery端如果向使用/service/echo服务，他后面不会添加instance字段，那么此时它就发现不到服务，所以我们特殊处理一下
        // 保留最后一个 / 之前的字符串，这才是真正的服务名 
        auto pos = service_instance.rfind('/');
        if (pos == std::string::npos) return service_instance;
        return service_instance.substr(0, pos);
    }
public:
    ServiceManager() {}
    // choose函数返回传入的服务对应的channel
    ServiceChannel::ChannelPtr choose(const std::string& service_name)
    {
        std::unique_lock<std::mutex> lock(_mutex);

        auto it = _services.find(service_name);
        if (it == _services.end()) {
            LOG_ERROR("当前没有能提供{}服务的节点!", service_name);
            return ServiceChannel::ChannelPtr();
        }
        return it->second->choose();
    }
    // 声明我关注哪些服务的上下线，不关心的我就不存放在_services中
    void declared(const std::string& service_name)
    {
        std::unique_lock<std::mutex> lock(_mutex);
        _follow_services.insert(service_name);
    }
    // etcd新增键值时，调用的回调接口，可使brpc掌握已上线服务的地址，从而完成rpc调用
    void onServiceOnline(const std::string& service_instance, const std::string& host)
    {
        // 获取真正的服务名
        std::string service_name = getServiceName(service_instance);
        // 定义，后面会赋值运算符重载
        ServiceChannel::ptr service;
        {
            // 1. 加锁
            std::unique_lock<std::mutex> lock(_mutex);
            // 2. 是否是关心的服务
            auto it = _follow_services.find(service_name);
            if (it == _follow_services.end()) {
                LOG_DEBUG("{}-{}服务上线了, 但是当前并不关心!", service_name, host);
                return;
            }
            // 3. 获取ServiceChannel的智能指针对象
            auto sit = _services.find(service_name);
            if (sit == _services.end()) {
                service = std::make_shared<ServiceChannel>(service_name);
                _services.insert(std::make_pair(service_name, service));
            } else {
                service = sit->second;
            }
        }
        if (!service) {
            LOG_ERROR("新增 {} 服务管理节点失败!", service_name);
            return ;
        }
        // 4. 添加 服务--地址 的映射
        service->append(host);
        LOG_DEBUG("{}-{} 服务上线新节点，进行添加管理!", service_name, host);
    }
    // etcd键值被删除时，调用的回调接口，从服务信道管理中，删除指定节点信道
    void onServiceOffline(const std::string& service_instance, const std::string& host)
    {
        // 获取真正的服务名
        std::string service_name = getServiceName(service_instance);
        // 定义，后面会赋值运算符重载
        ServiceChannel::ptr service;
        {
            // 1. 加锁
            std::unique_lock<std::mutex> lock(_mutex);
            // 2. 是否是关心的服务
            auto it = _follow_services.find(service_name);
            if (it == _follow_services.end()) {
                LOG_DEBUG("{}-{}服务下线了, 但是当前并不关心!", service_name, host);
                return;
            }
            // 3. 获取ServiceChannel的智能指针对象
            auto sit = _services.find(service_name);
            if (sit == _services.end()) {
                LOG_WARN("删除{}服务节点时，没有找到管理对象", service_name);
                return;
            } else {
                service = sit->second;
            }
        }
        // 4. 进行删除服务与地址映射的删除操作
        service->remove(host);
        LOG_DEBUG("{}-{} 服务下线节点，进行删除管理！", service_name, host);
    }
};
}